﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Collections.Concurrent;
using System.Diagnostics.Contracts;
using System.Reflection;
using System.Collections;
using System.Xml.Linq;
using SysConsole = System.Console;

using CommandXElement = King.Text.Xml.TXElement<King.Automation.XCommand.Tag>;
using CommandXAttribute = King.Text.Xml.TXAttribute<King.Automation.XCommand.Tag>;
using CommandXDeclaration = King.Text.Xml.TXDeclaration<King.Automation.XCommand.Tag>;

using King.Text;
using King.Extensions;
using King.Reflection;
using King.Xml;

namespace King.Automation {

    public class ConsoleException : CommandException {
        internal ConsoleException(string message) : base(message) { }
    }

    public abstract class AbstractConsole {

        public static readonly AbstractConsole SystemConsole = new SystemConsole();

        public abstract int CursorLeft { get; set; }
        public abstract int CursorTop { get; set; }

        public abstract ConsoleColor ForegroundColor { get; set; }
        public abstract ConsoleColor BackgroundColor { get; set; }

        public abstract TextWriter Out { get; }
        public abstract TextWriter Error { get; }
    }
    internal sealed class SystemConsole : AbstractConsole {

        public override int CursorLeft {
            get { return SysConsole.CursorLeft; }
            set { SysConsole.CursorLeft = value; }
        }
        public override int CursorTop {
            get { return SysConsole.CursorTop; }
            set { SysConsole.CursorTop = value; }
        }
        public override ConsoleColor ForegroundColor {
            get { return SysConsole.ForegroundColor; }
            set { SysConsole.ForegroundColor = value; }
        }
        public override ConsoleColor BackgroundColor {
            get { return SysConsole.BackgroundColor; }
            set { SysConsole.BackgroundColor = value; }
        }
        public override TextWriter Out {
            get { return SysConsole.Out; }
        }
        public override TextWriter Error {
            get { return SysConsole.Error; }
        }
    }

    public struct ConsoleCursorPosition {

        public static ConsoleCursorPosition GetCurrentPosition(AbstractConsole console) {
            return new ConsoleCursorPosition(console.CursorLeft, console.CursorTop);
        }

        private int m_left;
        private int m_top;

        public ConsoleCursorPosition(int left, int top) {
            m_left = left;
            m_top = top;
        }

        public int Left {
            get { return m_left; }
        }
        public int Top {
            get { return m_top; }
        }
        public void Set(AbstractConsole console) {
            console.CursorTop = m_top;
            console.CursorLeft = m_left;
        }

        public override string ToString() {
            return "Left=" + Left + ", Top=" + m_top;
        }
    }
    public sealed class ConsoleUpdate {

        private TextReader m_textReader;
        private ConsoleCursorPosition? m_position;
        private bool? m_toStandardOutput;
        private ConsoleColor? m_foreground;
        private ConsoleColor? m_background;
        private bool m_restoreConsoleState;

        public ConsoleUpdate(AbstractConsole console) {
        }
        public ConsoleUpdate(
            string text = null,
            TextReader textReader = null,
            ConsoleColor? foreground = null,
            ConsoleColor? background = null,
            ConsoleCursorPosition? position = null,
            bool? toStandardOutput = null,
            bool restoreConsoleState = false) {

            Contract.Requires(text == null || textReader == null);

            if (text != null)
                textReader = new StringReader(text);

            m_textReader = textReader;
            m_position = position;
            m_foreground = foreground;
            m_background = background;
            m_toStandardOutput = toStandardOutput;
            m_restoreConsoleState = restoreConsoleState;
        }

        public void Set(AbstractConsole console) {
            if (ForegroundColor != null)
                console.ForegroundColor = (ConsoleColor)ForegroundColor;
            if (BackgroundColor != null)
                console.BackgroundColor = (ConsoleColor)BackgroundColor;
        }

        public ConsoleColor? ForegroundColor {
            get { return m_foreground; }
        }
        public ConsoleColor? BackgroundColor {
            get { return m_background; }
        }
        public TextReader TextReader {
            get { return m_textReader; }
        }
        public bool RestoreConsoleState {
            get { return m_restoreConsoleState; }
        }
        public bool? ToStandardOutput {
            get { return m_toStandardOutput; }
        }

        public override string ToString() {
            return new TextJoin(", ") {
                m_position,
                new TextJoin(prefix: "Foreground=") { ForegroundColor },
                new TextJoin(prefix: "Background=") { BackgroundColor },
                new TextJoin(prefix: "ToStandardError=") { m_toStandardOutput == false ? "true" : null },
                new TextJoin(prefix: "RestoreConsoleState=") { m_restoreConsoleState == true ? "true" : null },
            }.ReadToEnd();
        }
    }

    public sealed class ConsoleMarkupInterpreter : IStreamable<ConsoleUpdate> {

        private struct ConsoleCursorColor {
            private ConsoleColor? m_foregound;
            private ConsoleColor? m_background;

            public ConsoleCursorColor(ConsoleColor? foreground, ConsoleColor? background) {
                m_foregound = foreground;
                m_background = background;
            }

            internal ConsoleColor? ForegroundColor { get { return m_foregound; } }
            internal ConsoleColor? BackgroundColor { get { return m_background; } }
        }

        private IEnumerator<ConsoleUpdate> m_enumerator;
        private XStreamReader m_xml;

        private int m_targetStandardError;
        private Stack<ConsoleCursorColor> m_cursorColorStack;

        public ConsoleMarkupInterpreter(TextReader reader) {
            m_cursorColorStack = new Stack<ConsoleCursorColor>();
            m_xml = new XStreamReader(reader, newLineChunking: true);
        }

        private IEnumerator<ConsoleUpdate> Generator() {

            while (m_xml.MoveNext()) {

                if (m_xml.Current is XElement)
                    OnElement();

                else if (m_xml.Current is TextReader)
                    yield return GenerateConsoleUpdate(
                        (TextReader)m_xml.Current);
            }

            yield return GenerateConsoleUpdate(new StringReader(Environment.NewLine));
        }
        private ConsoleUpdate GenerateConsoleUpdate(TextReader textReader) {
            var colors = m_cursorColorStack.Peek();
            return new ConsoleUpdate(
                textReader: textReader,
                foreground: colors.ForegroundColor,
                background: colors.BackgroundColor,
                toStandardOutput: m_targetStandardError == 0);
        }

        private void OnElement() {
            Contract.Assert(m_xml.Current is XElement);
            Contract.Assert(m_xml.IsStartElement || m_xml.IsEndElement);

            if (m_xml.IsStartElement)
                OnPush();

            if (m_xml.IsEndElement)
                OnPop();
        }
        private void OnPush() {
            Contract.Assert(m_xml.Current is XElement);
            Contract.Assert(m_xml.IsStartElement);

            var element = (XElement)m_xml.Current;

            m_cursorColorStack.Push(new ConsoleCursorColor(
                ((string)element.Attribute("foreground")).ToEnum<ConsoleColor?>(),
                ((string)element.Attribute("background")).ToEnum<ConsoleColor?>()
            ));

            if ((bool?)element.Attribute("toStandardError") == true)
                m_targetStandardError++;
        }
        private void OnPop() {
            Contract.Assert(m_xml.Current is XElement);
            Contract.Assert(m_xml.IsEndElement);

            var element = (XElement)m_xml.Current;

            if (m_cursorColorStack.Count > 1)
                m_cursorColorStack.Pop();

            if ((bool?)element.Attribute("toStandardError") == true)
                m_targetStandardError--;
        }

        public IEnumerator<ConsoleUpdate> GetEnumerator() {
            Contract.Requires(m_enumerator == null, "Only one enumerator can be generated per CommandInterpreter.");
            m_enumerator = Generator();
            return m_enumerator;
        }
        IEnumerator IEnumerable.GetEnumerator() {
            return GetEnumerator();
        }

        public ConsoleUpdate Current {
            get {
                if (m_enumerator == null)
                    throw new InvalidOperationException();
                return m_enumerator.Current;
            }
        }
        public bool MoveNext() {
            if (m_enumerator == null)
                m_enumerator = GetEnumerator();
            return m_enumerator.MoveNext();
        }
        public void Dispose() {
            m_xml.Dispose();
        }
        object IEnumerator.Current {
            get { return Current; }
        }
        public void Reset() {
            throw new NotSupportedException();
        }
    }

    [TextNodeContainsContent]
    public abstract class ConsoleElement : TextNodes {

        private ConsoleColor? m_foreground;
        private ConsoleColor? m_background;
        private bool m_toStandardError;

        internal ConsoleElement(
            ConsoleColor? foreground = null,
            ConsoleColor? background = null,
            bool toStandardError = false) {

            m_foreground = foreground;
            m_background = background;
            m_toStandardError = toStandardError;
        }

        protected override IEnumerable<object> Children(TextNodeContext context) {

            yield return new CommandXElement(XCommand.Console,
                inline: true,
                attributes: new[] { 
                    new CommandXAttribute("foreground", m_foreground),
                    new CommandXAttribute("background", m_background),
                    !m_toStandardError ? null : new CommandXAttribute("toStandardError", m_toStandardError),
                }) { 
                Children() 
            };
        }
    }
    public sealed class ConsoleMarkup : ConsoleElement {

        public ConsoleMarkup(
            ConsoleColor? foreground = null,
            ConsoleColor? background = null,
            bool toStandardError = false)
            : base(foreground, background, toStandardError) {
        }

        protected override IEnumerable<object> Children(TextNodeContext context) {

            // only emit ConsoleMarkup when in a ConsoleDocument
            if (context.Ancestors<ConsoleDocument>().None()) {
                yield return Children();
                yield break;
            }

            yield return base.Children(context);
        }
    }
    public sealed class ConsoleFlush : ConsoleElement {
    }

    [TextNodeSubstituteTargets(typeof(TextFlush))]
    public sealed class ConsoleDocument : ConsoleElement {

        public ConsoleDocument(
            ConsoleColor? foreground = null,
            ConsoleColor? background = null)
            : base(foreground, background, false) {
        }

        protected override IEnumerable<object> Substitute(TextNodeContext context, object value) {
            Contract.Assert(value is TextFlush);

            if (context.Descendents<ConsoleFlush>().Any())
                return null;

            return new ConsoleFlush() { value };
        }
        protected override IEnumerable<object> Children(TextNodeContext context) {

            // disallow nesting ConsoleMarkup roots
            if (context.Ancestors<ConsoleDocument>().Any())
                throw new ConsoleException("CommandContexts cannot be nested.");

            // ConsoleMarkup root is the start of an xml stream so emit a declaration
            yield return new CommandXDeclaration(version: "1.0", encoding: "utf-16");
            yield return Environment.NewLine;

            yield return base.Children(context);
        }
    }

    public sealed class ConsoleApplication {

        private const int BufferSize = 4096;

        public static int EntryPoint(string[] args, 
            Assembly assembly = null,
            AbstractConsole console = null,
            int errorLevelOnUnhandledException = -1) {

            // default to system console
            if (console == null)
                console = AbstractConsole.SystemConsole;

            // resolve the command name using the calling assembly if no assembly is explicitly supplied
            if (assembly == null)
                assembly = Assembly.GetCallingAssembly();
            var commandAssembly = CommandAssembly.GetAssembly(assembly);

            // activate the command (or help)
            try {
                //var command = commandAssembly.BindToCommand(args);
                object command = null;

                // pipe the ConsoleUpdates into a ConsoleApplication to render them to the console
                var application = new ConsoleApplication(command, console, errorLevelOnUnhandledException);

                // execute the application
                return application.Run();

            } catch (Exception e) {
                var color = console.ForegroundColor;
                console.ForegroundColor = ConsoleColor.Red;
                console.Error.WriteLine(e.Message);
                console.ForegroundColor = color;
                return -1;
            }
        }

        private AbstractConsole m_console;
        private IEnumerator<ConsoleUpdate> m_producer;
        private ConcurrentQueue<ConsoleUpdate> m_queue;
        private bool m_toStandardOutput;
        private int m_errorLevelOnUnhandledException;
        private char[] m_buffer;

        public ConsoleApplication(object command, 
            AbstractConsole console = null,
            int errorLevelOnUnhandledException = -1) {

            m_buffer = new char[BufferSize];
            m_errorLevelOnUnhandledException = errorLevelOnUnhandledException;

            if (console == null)
                console = AbstractConsole.SystemConsole;
            m_console = console;

            // flip the switch to tell the command to emit command markup
            var markup = new ConsoleDocument(
                foreground: console.ForegroundColor,
                background: console.BackgroundColor) { 
                    command 
                };

            // pipe the stream into a CommandInterpreter to convert the command markup to ConsoleUpdates
            m_producer = new ConsoleMarkupInterpreter(
                // convert the command from a tree of TextNodes into a TextStream using a TextGenerator
                new TextGenerator(markup)
            ).GetEnumerator();

            m_queue = new ConcurrentQueue<ConsoleUpdate>();
            m_toStandardOutput = true;
        }

        private void RenderUpdate(ConsoleUpdate update) {

            // save current position and console color so they can be restored later
            var originalCursor = new ConsoleUpdate(m_console);
            var originalToStandardOutput = m_toStandardOutput;

            // set position and color
            update.Set(m_console);

            if (update.ToStandardOutput != null)
                m_toStandardOutput = (bool)update.ToStandardOutput;

            // write text to the console in line chunks
            if (update.TextReader != null) {
                var stream = m_toStandardOutput ? m_console.Out : m_console.Error;

                // reading characters will likely execute commands so to give the user feedback on what
                // is happening it is very important to not buffer and instead to flush after each read
                int count = 0;
                while ((count = update.TextReader.Read(m_buffer, 0, BufferSize)) != 0) {
                    var line = new string(m_buffer, 0, count);
                    stream.Write(line);
                    stream.Flush();
                }
            }

            // restore console state
            if (update.RestoreConsoleState) {
                originalCursor.Set(m_console);
                m_toStandardOutput = originalToStandardOutput;
            }
        }
        private void Dequeue() {

            var count = m_queue.Count;
            while (count-- > 0) {
                ConsoleUpdate update;
                var result = m_queue.TryDequeue(out update);
                Contract.Assert(result);

                RenderUpdate(update);
            }
        }

        public void Enqueue(ConsoleUpdate update) {
            Contract.Assert(update != null);
            m_queue.Enqueue(update);
        }
        public int Run() {
            var consoleInitialColors = new ConsoleUpdate(
                foreground: m_console.ForegroundColor,
                background: m_console.BackgroundColor,
                textReader: new StringReader(Environment.NewLine));

            try {
                while (m_producer.MoveNext()) {
                    Enqueue(m_producer.Current);
                    Dequeue();
                }

            } catch (Exception e) {
                Command.ErrorLevel = m_errorLevelOnUnhandledException;
                Enqueue(new ConsoleUpdate(e.Message, foreground: ConsoleColor.Red));
                Dequeue();
            }

            Enqueue(consoleInitialColors);
            Dequeue();

            return Command.ErrorLevel;
        }
    }
}